#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""
@author: LiangChao
@email：kevinleong1011@hotmail.com
@desc:  被测对象的树形结构表示
"""
import inspect
import os
import sys
import time
import traceback
from datetime import datetime
from fnmatch import fnmatch

from makit import py, Path, fn
from .errors import InvalidTestError, TestBlockedError, TestFailedError, TestSkippedError
from .log import logger
from .utils import parse_class_info, parse_test_info

from .message import todo, confirm, warning

RUNNING = 'running'
BLOCKED = 'blocked'
PASSED = 'passed'
ERROR = 'error'
FAILED = 'failed'
SKIPPED = 'skipped'


class TestNode:
    """
    测试节点
    """

    def __init__(self, obj, project, context, fixture):
        self.obj = obj
        self.project = project
        self.context = context
        self.name, self.test_name, self.desc = parse_test_info(self.obj)
        self.fixture = fixture
        self._subs = []
        self.count = 0
        self._instance = None  # 类实例
        self._inherited_marks = []
        self.start_time, self.finish_time, self.time_cost = None, None, 0
        self.messages = []
        self.status = None
        self.outcome = None
        self.exception, self.error_summary, self.error_detail = None, None, None
        self.logger = logger
        self.pass_rate = 0

    @property
    def marks(self):
        marks = getattr(self.obj, 'marks', [])
        marks.extend(self._inherited_marks)
        return marks

    def add_sub(self, sub_obj):
        sub = TestNode(sub_obj, project=self.project, context=self.context, fixture=self.fixture)
        self._subs.append(sub)
        sub._inherited_marks.extend(self.marks)
        sub.load_tests()
        self.count += sub.count
        return sub

    def load_tests(self):
        if self._subs:
            return
        if inspect.ismodule(self.obj):
            self.load_test_module()
        elif inspect.isclass(self.obj):
            self.load_test_class()
        elif isinstance(self.obj, str):
            self.load_test_path()

    def load_test_module(self):
        """
        从模块中加载测试并返回数量
        :return:
        """
        pattern = self.context.data.pattern
        for name in dir(self.obj):
            if name.startswith('_'):
                continue
            v = getattr(self.obj, name)
            if inspect.isfunction(v):
                if pattern and not fnmatch(name, pattern):
                    continue
                if v.__module__ != self.obj.__name__:
                    continue
                if self.project.filter_test(v, self.context):
                    self.add_sub(v)
            elif inspect.isclass(v):
                if v.__module__ != self.obj.__name__:
                    continue
                self.add_sub(v)

    def load_test_class(self):
        """
        从类中加载测试项
        :return:
        """
        cli_pattern = self.context.data.pattern
        _, cls_name, _ = parse_class_info(self.obj)
        for name in dir(self.obj):
            if name.startswith('_'):
                continue
            if hasattr(self.obj, 'pattern') and not fnmatch(name, self.obj.pattern):
                continue
            if cli_pattern and not fnmatch(name, cli_pattern):  # 根据pattern过滤测试
                continue
            v = getattr(self.obj, name)
            if inspect.isfunction(v):
                if self.project.filter_test(v, self.context):
                    self.add_sub(v)

    def load_test_path(self):
        """
        从文件路径加载测试
        :return:
        """
        path = self.obj
        if os.path.isfile(path):
            if not path.endswith('.py'):
                raise InvalidTestError(f'Not a py: {path}')
            module = py.get_object(path)
            self.add_sub(module)
        else:
            for file in Path(path).search('*.py'):
                module = py.get_object(file)
                self.add_sub(module)

    def run(self):
        if inspect.ismodule(self.obj):
            self._run_module()
        elif inspect.isclass(self.obj):
            self._run_class()
        elif inspect.isfunction(self.obj):
            self._run_test()

    def _run_module(self):
        self.logger.debug(f'Start module: {self.test_name}')
        self.context.add_threading_node(self)
        setup = getattr(self.obj, 'setup', None)
        if setup:
            fn.run(setup, **self.context.data)
        for sub in self._subs:
            sub.run()
        teardown = getattr(self.obj, 'teardown', None)
        if teardown:
            fn.run(teardown, **self.context.data)

        self.context.reset(self)
        self.logger.debug(f'Finish module: {self.test_name}')

    def _run_class(self):
        self.logger.debug(f'Start class: {self.test_name}')
        self.context.add_threading_node(self)
        try:
            instance = object.__new__(self.obj)
            fn.run(instance.__init__, **self.context.data)
            for sub in self._subs:
                sub._instance = instance
                sub.run()
        finally:
            self.context.reset(self)
            self.logger.debug(f'Finish class: {self.test_name}')

    def _run_test(self):
        # 从原始方法对象获取参数
        def _run_parameters():
            params = getattr(self.obj, 'parameters', [])
            # 从context获取参数列表
            if not params:
                params.extend(self.context.parameters.get(self.test_name, []))
            if params:
                for p in params:
                    self.logger.info(f'Apply parameters: {p}')
                    _run_mtbf(**p)
            else:
                _run_mtbf()

        def _run_mtbf(**kwargs):
            repeat, duration = self.context.data.repeat, self.context.data.duration
            start = time.time()
            count = 0
            passed_count = 0
            if duration or repeat:
                while True:
                    if repeat and count >= repeat:
                        break
                    if duration and time.time() - start > duration:
                        break
                    count += 1
                    self.start_cycle(count)
                    _run_standard(**kwargs)
                    if self.outcome == PASSED:
                        passed_count += 1
                    self.finish_cycle(count)
                self.pass_rate = passed_count / count
                self.logger.info(f'Pass rate: {self.pass_rate * 100}%')
            else:
                _run_standard(**kwargs)

        def _run_standard(**kwargs):
            try:
                self.context.add_threading_node(self)
                self.start()
                if kwargs:
                    self.parameterize(kwargs)
                result = fn.run(self.obj, self._instance, **{**self.context.data, **kwargs})
                if result and isinstance(result, dict):
                    self.context.data.update(**result)
                self.success()
            except TestBlockedError:
                self.block()
            except (TestFailedError, AssertionError):
                self.fail()
            except TestSkippedError:
                self.skip()
            except Exception:
                self.error()
            finally:
                self.context.reset(self)
                self.finish()

        _run_parameters()

    def has_marks(self, *marks):
        return all(map(lambda x: x in self.marks, marks))

    def warning(self, message):
        self.messages.append(warning(message))

    def todo(self, message):
        self.messages.append(todo(message))

    def confirm(self, message):
        self.messages.append(confirm(message))

    def parameterize(self, params):
        self.logger.info(f'Run with params: {params}')
        self.project.plugin.on_parameterized(self, self.context, params)

    def start_cycle(self, cycle):
        self.logger.info(f'-------------------- repeat {cycle} --------------------')
        self.project.plugin.on_repeat_start(self, self.context, cycle)

    def finish_cycle(self, cycle):
        self.logger.info(f'---------------- finish repeat {cycle} ------------------')
        self.project.plugin.on_repeat_finished(self, self.context, cycle)

    def start(self):
        self.status = RUNNING
        self.logger.debug(f'Start script: {self.test_name}')
        self.start_time = datetime.now()
        self.project.plugin.on_script_started(self, self.context)

    def finish(self):
        self.status = None
        self.finish_time = datetime.now()
        self.time_cost = (self.finish_time - self.start_time).seconds
        self.project.plugin.on_script_finished(self, self.context)
        self.logger.info(f'Finish test: {self.test_name}, cost {self.time_cost}s')
        logger.debug('-' * 50)

    def fail(self):
        self.outcome = FAILED
        self.project.plugin.on_script_failed(self, self.context)
        self.logger.error(f'Failed: {self.test_name}')
        self._catch_error()
        self.fixture.failed_count += 1

    def block(self):
        self.outcome = BLOCKED
        self.fixture.blocked_count += 1
        self.project.plugin.on_script_blocked(self, self.context)
        self.logger.debug(f'Blocked: {self.test_name}')

    def success(self):
        self.outcome = PASSED
        self.fixture.passed_count += 1
        self.project.plugin.on_script_passed(self, self.context)
        self.logger.success(f'Passed: {self.test_name}')

    def skip(self):
        self.outcome = SKIPPED
        self.fixture.skipped_count += 1
        self.project.plugin.on_script_skipped(self, self.context)
        self._catch_error()
        self.logger.success(f'Skipped: {self.test_name}')

    def error(self):
        self.project.plugin.on_script_error(self, self.context)
        self._catch_error()
        self.outcome = ERROR
        self.fixture.error_count += 1
        self.logger.error(f'Error: {self.test_name}')

    def _catch_error(self):
        e_type, e, b = sys.exc_info()
        self.exception = e
        self.error_summary = str(e)
        tb = traceback.format_exc()
        self.error_detail = tb
        self.logger.error(traceback.format_exc())

    def __repr__(self):
        return f'<TestNode {self.test_name}>'
